fix(types): declare the component-node arm of DashboardComponentSchema.widgets on the TypeScript face - #8296
Merged
Conversation
…a.widgets on the TypeScript face `widgets` was `DashboardWidgetSchema[]` while the zod twin has carried a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the slot (`type: 'metric-card'`, passthrough `BaseSchema` body) or a spec-family widget. Every `metric-card` block the plugin-dashboard README teaches parsed green and `tsc --strict` refused it (6 x TS2561 on `value` at fc32921). Ruled option (a), decision batch #68. - `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES`; exported (declaration emit needs a name, TS4033) and added to the barrel. - `widgets: Array<DashboardWidgetSlotComponentSchema | DashboardWidgetSchema>`, component arm first, as in the zod twin. `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue`. - Two-face pin `dashboard-widget-slot-component-arm-7952.test.ts`: README shape compiles and parses; forbidden widening still refused; discriminated refusals hold; the measured union corner (type-less envelope + stray key compiles, zod refuses by name) recorded, not contracted. - README "TypeScript Support" table and prose corrected to the two-arm slot. - Changeset: `@object-ui/types` minor (published TS accept set widens). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
…hboard-widgets-union-arm
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…xported Contract review on PR #8296 measured the docblock's claim that TS4033 forces the export as false: in this package's own build (declaration + composite, TypeScript 6.0.3) a non-exported arm referenced from the exported DashboardComponentSchema emits into dist/complex.d.ts as a local interface, exit 0, and a barrel consumer still writes the node with no name. - arm docblock: the export is a deliberate authoring-surface decision — a name an author can annotate the node with — taken intentionally in the opposite direction to the zod twin's non-exported const. - "the second arm" → the component arm, first in the declaration as in zod (complex.ts closed-set docblock, plugin-dashboard README). Comment and prose only; no declaration, pin, changeset or barrel change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
September 7, 2026 11:09
os-zhuang
pushed a commit
that referenced
this pull request
Sep 7, 2026
…r key objectui#8252. `WiderThanDeclared` recorded one prose class per ENTRY, and the clause for `DashboardComponentSchema.widgets` was three words — "`widgets` is SCHEMA-NODE" — true of its second arm and false of its first, a concrete component node the TypeScript face was missing whole (objectui#7952, declared by #8296 as c842594). A per-KEY verdict over a per-ARM fact. `WIDER_ARMS` records one verdict per union arm of each ledgered key. The arm ENUMERATION is measured from the mirror values (`measureMirrorArms`); the verdicts are judged. Pinned: every ledgered key has exactly one row, every row names one verdict per MEASURED arm, and objectui#7952's key cannot collapse to a single verdict. The header gains this ledger's figures — the fifth ledger and the only one objectui#7733 / objectui#8222 did not reach, which is how "27 SCHEMA-NODE keys" could be quoted while the re-derived figure is 25. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7952
What
DashboardComponentSchema.widgets(packages/types/src/complex.ts) becomes the two-arm union the zod twin has carried since the 2026-08-14 ruling (objectstack#8593): an array ofDashboardWidgetSlotComponentSchema | DashboardWidgetSchema, component arm first as inzod/complex.zod.ts.DashboardWidgetSlotComponentSchemais new —extends BaseSchemawithtype: DashboardComponentWidgetType— and is exported from@object-ui/types(added to the barrel).packages/types/src/zod/complex.zod.tsis untouched.DashboardWidgetSchemais NOT widened withvalue/icon/trend/trendValue.Ruling applied as written (director seat, decision batch #68, 2026-09-07, maintainer 「同意」; card comment 5565355483) — not re-opened here.
Spelling choice (the brief asked which and why)
An
interface … extends BaseSchemarather than an intersection or a discriminated member:BaseSchema.extend({ type: z.enum(DASHBOARD_COMPONENT_WIDGET_TYPES) }), and the same shapeDashboardComponentSchemaitself uses (extends BaseSchemaplus a literaltype);declaration+composite, TypeScript 6.0.3): a non-exported arm referenced from the exportedDashboardComponentSchemabuilds exit 0, emits intodist/complex.d.tsas a local interface, and a barrel consumer still writes the node with no name. It is exported so an author can annotate the node the platform accepts (the README teaches it), an intentional asymmetry with the zod twin's non-exported arm, whose docblock records why that const stays private. (Corrected in the patch round: the first version of this bullet claimed TS4033 forced the export — false, per the contract review.)BaseSchema's[key: string]: anyis the passthrough, so the component's own props reach the compiler through the index signature and never throughDashboardWidgetSchema.Measurements (re-driven before and after; both PM mechanism assumptions measured, not inherited)
Before, on
fc32921. The sixconst schema = { type: 'dashboard', … }blocks ofpackages/plugin-dashboard/README.md, extracted from the page's own bytes, annotatedDashboardComponentSchemaand compiled--strictagainst the source declaration: 6 × TS2561 ('value' does not exist in type 'DashboardWidgetSchema'. Did you mean to write 'values'?) — the card's five across:47/:177/:274, plus one in the "Dashboard with Charts" block at:216, which PR #7951 rewrote after the card was measured. Runtime, same shape:DashboardComponentSchema.safeParseACCEPT, all seven authored keys preserved.After, on
297b014. Same six blocks, same annotation, same harness: 0 errors. The runtime verdict is unchanged (the zod face was not edited). The edited README "TypeScript Support" block also compiles against the BUILTpackages/types/dist/index.d.ts— thecheck:doc-snippetsquestion, narrowed to the one block this PR edits (exit 0).Corners, measured on the source after the change (each is also pinned):
widgets[]{ type: 'bar', title, bogus: 1 }typeunrecognized_keys[bogus]{ type: 'metric-card', someProp: 1 }{ type: 'not-a-component', value }{ id, component: {…}, layout }— legacy envelope, notype{ id, component: {…}, bogus: 1 }— legacy envelope plus a stray keyunrecognized_keys[bogus]const w: DashboardWidgetSchema = { type: 'metric-card', value }The stray-key envelope row is the one cost of a TypeScript union with a passthrough arm. It is recorded in the member's docblock and pinned two-faced (
tscaccepts, zod refuses by name) so it reads as a measured limit rather than a hatch; the runtime is the strict face on that corner, as it already is for everyBaseSchemaslot.Consumer cost, measured. The arm is assignable to
DashboardWidgetSchema, so every(w: DashboardWidgetSchema)callback inplugin-dashboardcompiled unchanged (pinned). An UNANNOTATED element read —schema.widgets.mapwith a barewreadingw.layout, atDashboardGridLayout.tsx:96/:161,DashboardWithConfig.tsx:155,DashboardEditor.tsx:559— now resolves through the index signature toanyinstead of the widget's declared type — it compiles and checks less. Stated in the changeset; not repaired here: it isBaseSchema's ownany, the one everySchemaNodeconsumer already carries, and annotating those sites is a plugin-dashboard / plugin-designer change outside this card's surface.Pin —
packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.tsTwo-faced, enforced by
tsconfig.test.jsonthroughtype-check(objectui#3009): the README shape compiles AND parses with its keys kept;DashboardWidgetSchemastill refusesvalue(an@ts-expect-errorthat becomes TS2578 if anyone widens it); the arm'stypeis closed by reference; the slot element IS the two-arm union; the arm is assignable toDashboardWidgetSchema; the discriminated refusals hold on both faces; the envelope corner is recorded as above.Reverse verification — committed first; run on
297b014inside the verify lock; script with a restore trap.widgetsput back toDashboardWidgetSchema[](on-disk proof: two-arm spelling 1 to 0, one-arm spelling 0 to 1), thentsc -p tsconfig.test.json: exit 2, 4 errors, all four in the new pin —:82TS2561 onvalue,:101the two-armEqualcollapsing tofalse,:165and:184the two passthrough literals turning into TS2353. Predicted two, measured four: the direction is red as expected, and the extra two are the passthrough-arm literal and the envelope-corner literal, both excess-property errors once the arm is gone. Restore proved by blob hash —git hash-objectafter restore equalsHEAD:packages/types/src/complex.ts(b7571577),git diff HEAD --statempty. The ablation resolves through the tests' relative../complex.jsimports (source, notdist/), so no build leg was involved.Gates — final head
297b014; every exit captured to disk before any output was readpnpm --filter @object-ui/types type-check(build project + examples + tests)pnpm exec vitest run packages/types/WiderThanDeclared['complex.zod.ts#DashboardComponentSchema']still listswidgets, its remaining reason being the SCHEMA-NODEcomponentslot of the second arm — the per-arm re-judgement is objectui#8252'spnpm --filter @object-ui/plugin-dashboard type-check(13-package dependency closure built first)pnpm --filter @object-ui/plugin-designer type-check(closure built first)readme-dashboard-examples-spec-valid.test.ts(plugin-dashboard)check:spec-symbols·check:doc-types·check:doc-fences·check:control-bytes·check-changeset-presencelint, narrowedeslint --format jsonover the 3 changed.tsfiles: 3 files judged, 0 errors, 10 pre-existingno-explicit-anywarnings on lines this PR does not touch. Population read fromeslint.config.js(files: ['**/*.{ts,tsx}']blocks). Invariance: noparserOptions.project/projectService(linting is not type-aware) and no custom rule undereslint-rules/reads other files, so this diff cannot move any untouched file's verdictcheck:doc-snippetsdist/(its own--build-filterlists 34 packages). The narrowed reading above (edited block against the built types.d.ts, exit 0) stands in; the whole-farm run is CI'scheck:readme-exports@object-ui/types, a cross-package import outside that gate's self-import ruleapp-shelltype-checkwidgetsreads all go through(draft as any).widgetscasts, so the element union cannot reach them验收备注
packages/plugin-dashboard/README.md:131still declareswidgets: DashboardWidgetSchema[]in the "Dashboard Schema" block — valid TS (the narrower array is assignable) and it teaches the spec-family usage; left as is.zod-mirror-parity.test.ts'sWiderThanDeclarednote "widgetsis SCHEMA-NODE" is now true of the whole key (the first arm is aligned); rewording it per arm is objectui#8252's census, whose file surface it is.content/docs/api/schema-reference.md's dashboard table teachescolSpan/rowSpan/bodyas widget keys (all three refused by name — reproducer in the card) andrefreshIntervalin milliseconds where the renderer multiplies by 1000; that row'swidgetstype column is also one arm short after this PR — one docs edit, out of this card's surface.Route
Draft,
needs:contract-review(clause ②: a published TypeScript accept set widens;@object-ui/typeschangeset, minor). Reviewed in-seat before any ready / queue / auto-merge step — none taken here. Related: objectui#8252 (the census ordered by this card's ruling, held behind this PR), objectui#7035 / PR #7951 (the README's docs half), objectui#6002 (the.strict()ruling the refusals rest on).Implemented in Claude Code session
session_01QtGhnU3WnnWyiWeYQhw2aX(PM loop round R1,domain:specseat).🤖 Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code